package com.wxm.log.api.aspect;

import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.extra.servlet.ServletUtil;
import cn.hutool.json.JSONObject;
import com.wxm.log.api.annotation.Log;
import com.wxm.log.api.entity.OpLogEntity;
import com.wxm.log.api.event.LogEvent;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.CodeSignature;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.stream.Stream;

import com.wxm.log.core.R;

/**
 * <p>
 *      日志切面
 * </p>
 *
 * @author: wxm
 * @Date: 2021/10/5
 */
@Aspect
public class LogAspect {
    /**
     * 事件发布是由ApplicationContext对象管控的，我们发布事件前需要注入ApplicationContext对象调用publishEvent方法完成事件发布
     **/
    @Autowired
    private ApplicationContext applicationContext;

    /**
     * 线程安全的容器放入 OpLogEntity
     */
    private static final ThreadLocal<OpLogEntity> THREAD_LOCAL = new ThreadLocal<>();

    /**
     * Log注解切入点
     */
    @Pointcut("@annotation(com.wxm.log.api.annotation.Log)")
    public void log() {
    }

    /**
     * 前置 进行请求参数等拦截
     */
    @Before(value = "log()")
    public void before(JoinPoint joinPoint){
        OpLogEntity log = this.getLogEntity();
        log.setStartTime(LocalDateTime.now());//开始时间

        //得到request
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        log.setIp(ServletUtil.getClientIP(request));//得到IP
        log.setType(1);//默认正常日志
        log.setUserId(1);
        log.setUserName("内置用户");

        Class<?> aClass = joinPoint.getTarget().getClass();//Controller类
        String methodName = joinPoint.getSignature().getName();//方法名称

        Log logAnnotation = this.getLogAnnotation(joinPoint);
        log.setDescription(logAnnotation.value());//说明
        log.setClassPath(aClass.getName());//类路径
        log.setActionMethod(methodName);//获取执行的方法名
        log.setRequestUrl(URLUtil.getPath(request.getRequestURI()));//请求url
        log.setRequestType(request.getMethod());//请求类型
        log.setUa(StrUtil.sub(request.getHeader("user-agent"), 0, 500));//浏览器

        //是否记录参数
        if (logAnnotation.recordRequestParam()){
            Object[] args = joinPoint.getArgs(); // 参数值
            String[] argNames = ((MethodSignature)joinPoint.getSignature()).getParameterNames(); // 参数名

            JSONObject jsonObject = new JSONObject();
            for (int i = 0; i < argNames.length; i++) {
                jsonObject.put(argNames[i],args[i]);
            }
            log.setParams(jsonObject.toString());
        }
        THREAD_LOCAL.set(log);
    }

    /**
     * 后置 进行返回值设置
     * @param r R
     */
    @AfterReturning(returning = "r", pointcut = "log()")
    public void after(JoinPoint joinPoint,R r){
        Log logAnnotation = this.getLogAnnotation(joinPoint);
        OpLogEntity log = this.getLogEntity();
        if (!r.getCode().equals(200)){
            log.setType(2);//异常日志类型
            log.setExDesc(r.getMsg());//异常说明
        }

        //判断是否记录返回值
        if (logAnnotation.recordResponseParam() && null != r) {
            log.setResult(r.toString());
        }
        this.publishEvent(log);
    }

    /**
     * 异常通知
     * @param e 异常
     */
    @AfterThrowing(pointcut = "log()", throwing = "e")
    public void doAfterThrowable(Throwable e) {
        OpLogEntity log = this.getLogEntity();
        log.setType(2);//异常类型
        log.setExClass(e.getClass().getName());//异常类
        log.setExDesc(e.getMessage());//异常说明
        this.publishEvent(log);
    }

    /**
     * 发布日志事件[补全日志完成时间和耗时]
     * @param log 日志实体
     */
    private void publishEvent(OpLogEntity log) {
        log.setEndTime(LocalDateTime.now());//完成时间
        log.setConsumingTime(log.getStartTime().until(log.getEndTime(), ChronoUnit.MILLIS));//耗时 毫秒
        applicationContext.publishEvent(new LogEvent(log));//发布事件
        THREAD_LOCAL.remove();//当前线程释放OpLogEntity
    }


    /**
     * 获取ThreadLocal中的OpLogEntity/如果没有则进行new
     * @return OpLogEntity
     */
    private OpLogEntity getLogEntity() {
        OpLogEntity log = THREAD_LOCAL.get();
        if (log == null) {
            return new OpLogEntity();
        }
        return log;
    }

    /**
     * 获取切面的Log注解
     * @param joinPoint JoinPoint
     * @return Log
     */
    private Log getLogAnnotation(JoinPoint joinPoint){
        // 从切面织入点处通过反射机制获取织入点处的方法
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 获取切入点所在的方法
        Method method = signature.getMethod();
        // 获取操作
        return method.getAnnotation(Log.class);
    }
}
